package cn.net.isir.core;

import org.apache.log4j.Logger;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;

import java.util.Collection;

/**
 * @author liuxubo
 * @filename PhoneModularRealmAuthenticator.java
 * @package cn.net.isir.core
 * @email lxb@isir.net.cn
 * @date 2019/01/07
 * @comment
 * @since 1.8
 */
public class PhoneModularRealmAuthenticator extends ModularRealmAuthenticator {

    //声明日志对象
    private static Logger logger = Logger.getLogger(PhoneModularRealmAuthenticator.class);

    /**
     * Attempts to authenticate the given token by iterating over the internal collection of
     * {@link Realm}s.  For each realm, first the {@link Realm#supports(AuthenticationToken)}
     * method will be called to determine if the realm supports the {@code authenticationToken} method argument.
     * <p/>
     * If a realm does support
     * the token, its {@link Realm#getAuthenticationInfo(AuthenticationToken)}
     * method will be called.  If the realm returns a non-null account, the token will be
     * considered authenticated for that realm and the account data recorded.  If the realm returns {@code null},
     * the next realm will be consulted.  If no realms support the token or all supporting realms return null,
     * an {@link AuthenticationException} will be thrown to indicate that the user could not be authenticated.
     * <p/>
     * After all realms have been consulted, the information from each realm is aggregated into a single
     * {@link AuthenticationInfo} object and returned.
     *
     * @param authenticationToken the token containing the authentication principal and credentials for the
     *                            user being authenticated.
     * @return account information attributed to the authenticated user.
     * @throws IllegalStateException   if no realms have been configured at the time this method is invoked
     * @throws AuthenticationException if the user could not be authenticated or the user is denied authentication
     *                                 for the given principal and credentials.
     */
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
//        return super.doAuthenticate(authenticationToken);
        assertRealmsConfigured();
        // 强制转换回自定义的CustomizedToken
        ShiroUsernamePasswordPhoneToken phoneToken = (ShiroUsernamePasswordPhoneToken) authenticationToken;
        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 区分单个和多个
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), phoneToken);
        } else {
            return doMultiRealmAuthentication(realms, phoneToken);
        }
    }

    /**
     * Performs the multi-realm authentication attempt by calling back to a {@link AuthenticationStrategy} object
     * as each realm is consulted for {@code AuthenticationInfo} for the specified {@code token}.
     *
     * @param realms the multiple realms configured on this Authenticator instance.
     * @param token  the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials.
     * @return an aggregated AuthenticationInfo instance representing account data across all the successfully
     * consulted realms.
     */
    @Override
    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) throws AuthenticationException {
        AuthenticationException finalException = new AuthenticationException();
        AuthenticationStrategy strategy = getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
        logger.info("Iterating through {" + realms.size() + "} realms for PAM authentication");

        for (Realm realm : realms) {
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
            if (realm.supports(token)) {
                logger.info("Attempting to authenticate token [{" + token.toString() + "}] using realm [{" + realm.toString() + "}]");
                AuthenticationInfo info = null;
                Throwable t = null;
                try {
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable throwable) {
                    try {
                        finalException = (AuthenticationException) throwable;
                    } catch (Exception ignored) {
                    }
                    t = throwable;
                    logger.info("Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:");
                }
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
            } else {
                logger.info("Realm [{" + realm.toString() + "}] does not support token {" + token.toString() + "}.  Skipping realm.");
            }
        }
        logger.info(aggregate);
        // 如果没有用户信息,说明报错了,返回报错异常
        if (aggregate.getPrincipals() == null) {
            throw finalException;
        }
        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }
}
